﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.ComponentModel;
using System.IO;
using System.Drawing;

namespace ESpace
{
    public delegate DBItem LoadRecDelegate(EStream st, string ext, DBItem Parent);

    // класс для создания древовидной структуры папок и записей, который загружается как база данных весь сразу
    // стандартная папка имеет пустое расширение и класс такой папки без наворотов ниже
    public abstract class DBItem : IComparable<DBItem>
    {
        public static LoadRecDelegate LoadRec = null;

        /// <summary>Родительская папка</summary>
        public DBItem Parent;

        /// <summary>Имя элемента. Должно быть уникальным для одинакового расширения</summary>
        public string Name;

        public abstract string Ext { get; }

        /// <summary>Элемент может иметь вложеные записи</summary>
        public bool IsDirectory { get { return Items != null; } set { if (value)Items = new List<DBItem>(); else Items = null; } }

        public string NameExt { get { return Name + "." + Ext; } }

        public List<DBItem> Items = null;

        /// <summary>Весь путь от корневого элемента до текущего элемента включительно, включая расширения всех элементов. Поле является уникальным именем объекта</summary>
        public string FullPath
        {
            get
            {
                if (Parent == null) return NameExt;

                DBItem dr = Parent;
                string ret = NameExt;
                // пробегаемся пока не достигнем корневой директории
                while (dr != null)
                {
                    ret = dr.NameExt + "\\" + ret;
                    dr = dr.Parent;
                }
                return ret;
            }
        }

        /// <summary>Весь путь от корневого элемента до текущего элемента включительно, без расширений</summary>
        public string FPath
        {
            get
            {
                if (Parent == null) return Name;

                DBItem dr = Parent;
                string ret = Name;
                // пробегаемся пока не достигнем корневой директории
                while (dr != null)
                {
                    ret = dr.Name + "\\" + ret;
                    dr = dr.Parent;
                }
                return ret;
            }
        }

        public DBItem(EStream st, DBItem Parent)
        {
            if (LoadRec == null) throw new Exception("Не указана статическая функция для загрузки записей по расширениям 'LoadRec'");

            this.Parent = Parent;

            #region *** загрузка ***
            Name = st.LoadStr();
            // элементы
            if (st.LoadBool())
            {
                IsDirectory = true;
                int len = st.LoadInt();
                for (int i = 0; i < len; i++)
                {
                    string ext = st.LoadStr();
                    Items.Add(LoadRec(st, ext, this));
                    //Items.Add(Load(st, ext, this));
                }
            }

            #endregion *** загрузка ***
        }

        public virtual void Save(EStream st)
        {
            st.SaveStr(Name);

            // вложеные элементы
            st.SaveBool(IsDirectory);
            if (IsDirectory)
            {
                st.SaveInt(Items.Count);
                for (int i = 0; i < Items.Count; i++)
                {
                    st.SaveStr(Items[i].Ext);
                    Items[i].Save(st);
                }
            }
        }

        public DBItem(String Name, bool IsDirectory, DBItem Parent)
        {
            this.Name = Name;
            this.Parent = Parent;
            this.IsDirectory = IsDirectory;
        }

        public int CompareTo(DBItem rec)
        {
            int ret = IsDirectory.CompareTo(rec.IsDirectory) * -1;
            if (ret != 0) return ret;
            return NameExt.ToUpper().CompareTo(rec.NameExt.ToUpper());
        }

        #region *** поиск записей *****************************************

        /// <summary> Возвращает все записи в каталоге с заданными расширениями </summary>
        /// <param name="ExtToFind">расшарения, записи с которыми нужно найти</param>
        public List<DBItem> GetRecords(string[] ExtToFind)
        {
            List<DBItem> ret = new List<DBItem>();
            FindRecords(ExtToFind, false, null, this, ret);
            return ret;
        }

        /// <summary> Возвращает все записи в каталоге и во ВСЕХ вложеных каталогах с заданными расширениями </summary>
        /// <param name="ExtToFind">расшарения, записи с которыми нужно найти</param>
        public List<DBItem> GetAllRecords(string[] ExtToFind)
        {
            List<DBItem> ret = new List<DBItem>();
            FindRecords(ExtToFind, true, null, this, ret);
            return ret;
        }

        /// <summary> Возвращает все записи в каталоге и во ВСЕХ вложеных каталогах с заданными расширениями </summary>
        /// <param name="ExtToFind">расшарения, записи с которыми нужно найти</param>
        /// <param name="ExcludeDirectories">имена каталогов, в которых не нужно искать</param>
        public List<DBItem> GetAllRecords(string[] ExtToFind, string[] ExcludeDirectoriesFullPath)
        {
            List<DBItem> ret = new List<DBItem>();
            FindRecords(ExtToFind, true, ExcludeDirectoriesFullPath, this, ret);
            return ret;
        }

        void FindRecords(string[] ExtToFind, bool FindInSubDirectories, string[] ExcludeDirFullPath, DBItem dir, List<DBItem> ret)
        {
            // проверка на непроверяемые директории
            if (ExcludeDirFullPath != null)
            {
                for (int i = 0; i < ExcludeDirFullPath.Length; i++)
                {
                    if (dir.FullPath.ToUpper() == ExcludeDirFullPath[i].ToUpper()) return;
                }
            }

            // заполнение списка записей с учетов фильтров
            for (int i = 0; i < dir.Items.Count; i++)
            {
                bool bAdd = true;
                if (ExtToFind != null)
                {
                    bAdd = false;
                    for (int j = 0; j < ExtToFind.Length; j++)
                        if (dir.Items[i].Ext.ToUpper() == ExtToFind[j].ToUpper()) { bAdd = true; break; }

                }

                // если производить поиск и в поддиректориях и это директория
                if (FindInSubDirectories && dir.Items[i].IsDirectory)
                    FindRecords(ExtToFind, FindInSubDirectories, ExcludeDirFullPath, dir.Items[i], ret);


                if (bAdd) ret.Add(dir.Items[i]);
            }

        }

        #endregion *** поиск записей *****************************************

        #region *** поиск каталогов *****************************************

        /// <summary> Возвращает все каталоги в каталоге </summary>
        public List<DBItem> GetDirectories(bool FindInSubDirectories)
        {
            return GetDirectories(null, false, null);
        }

        public List<DBItem> GetDirectories(string[] ExtToFind, bool FindInSubDirectories, string[] ExcludeDirectoriesFullPath)
        {
            List<DBItem> ret = new List<DBItem>();
            FindRecords(ExtToFind, FindInSubDirectories, ExcludeDirectoriesFullPath, this, ret);

            for (int i = 0; i < ret.Count; i++)
            {
                if (!ret[i].IsDirectory)
                {
                    ret.RemoveAt(i);
                    i--;
                }
            }

            return ret;
        }

        #endregion *** поиск каталогов *****************************************

        public int IndexOfRecord(string Name, string Ext) { return IndexOfRecord(Name + "." + Ext); }

        public int IndexOfRecord(string RecNameExt)
        {
            RecNameExt = RecNameExt.ToUpper();
            for (int i = 0; i < Items.Count; i++) if (RecNameExt == Items[i].NameExt.ToUpper()) return i;
            return -1;
        }

        /// <summary>Возвращает уникальное имя отталкиваясь от желаемого имени srsName и расширения ext. (Если элемент с таким именем существует в каталоге, добавляет в скобочках цифру)</summary>
        public string GetUnicName(string srsName, string ext)
        {
            int n = 0;
            string ret = srsName;

            // смотрим, нет ли уже вконце цифры в скобочках. И если есть, убираем ее и скобочки из исходного имени
            if (srsName[srsName.Length - 1] == ')')
            {
                int ind = srsName.LastIndexOf('(');
                if (ind != -1 && ind < (srsName.Length - 2))
                {
                    int tmpInt = 0;
                    if (int.TryParse(srsName.Substring(ind + 1, srsName.Length - ind - 2), out tmpInt))
                    {
                        string str = srsName.Substring(0, ind).Trim();
                        if (str != "") { srsName = str; n = tmpInt; }
                    }
                }
            }

            while (true)
            {
                int ind = IndexOfRecord(ret, ext);
                if (ind == -1) return ret;
                n++;
                ret = srsName + "(" + n.ToString() + ")";
            }
        }

        /// <summary> Поиск элемента по пути </summary>
        /// <param name="fullPath">полный путь</param>
        /// <param name="db">корневой каталог от которого начинается путь</param>
        /// <returns></returns>
        public static DBItem GetItemByPath(string fullPath, DBItem db)
        {
            DBItem ret = db;
            string[] sl = fullPath.Split('\\');
            if (sl[0].ToUpper() != db.NameExt.ToUpper()) return null;

            for (int i = 1; i < sl.Length; i++)
            {
                int ind = ret.IndexOfRecord(sl[i]);
                if (ind == -1) return null;
                ret = ret.Items[ind];
            }
            return ret;
        }

        /// <summary>
        /// Перемещение дочерних элементов в элемент destDir
        /// Возаращает индексы всех Полностью скопированных элементов (частично скопированные, то есть подкаталоги, в которых остались не скопированные элементы не входят)
        /// </summary>
        /// <param name="moveIndList">индексы всех полностью перемещенных элементов</param>
        /// <param name="destDir">элемент, куда копируются дочерние элементы</param>
        /// <param name="GetActionWithDupl">функция, возвращающая, что делать с дубликатом: Не заменять, заменять, дописать (скопировать дочерние элементы копируемого элемента-каталога в элемент-каталог-дубликат)</param>
        /// <param name="AfterMoveItemEvent">событие переноса элемента. Возникает После переноса</param>
        public List<int> MoveItems(List<int> moveIndList, DBItem destDir, GetActionWithDuplDelegate GetActionWithDupl, OnMoveDelegate AfterMoveItemEvent)
        {
            return CopyItems(moveIndList, destDir, GetActionWithDupl, null, AfterMoveItemEvent, true);
        }

        /// <summary>
        /// Копирование дочерних элементов в элемент destDir
        /// Возаращает индексы всех Полностью скопированных элементов (частично скопированные, то есть подкаталоги, в которых остались не скопированные элементы не входят)
        /// </summary>
        /// <param name="moveIndList">индексы всех полностью перемещенных элементов</param>
        /// <param name="destDir">элемент, куда копируются дочерние элементы</param>
        /// <param name="GetActionWithDupl">функция, возвращающая, что делать с дубликатом: Не заменять, заменять, дописать (скопировать дочерние элементы копируемого элемента-каталога в элемент-каталог-дубликат)</param>
        /// <param name="AfterCopyItemEvent">событие копирования элемента. Возникает После копирования</param>
        public List<int> CopyItems(List<int> moveIndList, DBItem destDir, GetActionWithDuplDelegate GetActionWithDupl, OnMoveDelegate AfterCopyItemEvent)
        {
            return CopyItems(moveIndList, destDir, GetActionWithDupl, null, AfterCopyItemEvent, false);
        }

        /// <summary>
        /// Перемещение дочерних элементов в элемент destDir
        /// Возаращает индексы всех Полностью скопированных элементов (частично скопированные, то есть подкаталоги, в которых остались не скопированные элементы не входят)
        /// </summary>
        /// <param name="moveIndList">индексы всех полностью перемещенных элементов</param>
        /// <param name="destDir">элемент, куда копируются дочерние элементы</param>
        /// <param name="GetActionWithDupl">функция, возвращающая, что делать с дубликатом: Не заменять, заменять, дописать (скопировать дочерние элементы копируемого элемента-каталога в элемент-каталог-дубликат)</param>
        /// <param name="BeforeMoveItemEvent">событие переноса элемента. Возникает Перед переносом</param>
        /// <param name="AfterMoveItemEvent">событие переноса элемента. Возникает После переноса</param>
        public List<int> MoveItems(List<int> moveIndList, DBItem destDir, GetActionWithDuplDelegate GetActionWithDupl, OnMoveDelegate BeforeMoveItemEvent, OnMoveDelegate AfterMoveItemEvent)
        {
            return CopyItems(moveIndList, destDir, GetActionWithDupl, BeforeMoveItemEvent, AfterMoveItemEvent, true);
        }

        /// <summary>
        /// Копирование дочерних элементов в элемент destDir
        /// Возаращает индексы всех Полностью скопированных элементов (частично скопированные, то есть подкаталоги, в которых остались не скопированные элементы не входят)
        /// </summary>
        /// <param name="moveIndList">индексы всех полностью перемещенных элементов</param>
        /// <param name="destDir">элемент, куда копируются дочерние элементы</param>
        /// <param name="GetActionWithDupl">функция, возвращающая, что делать с дубликатом: Не заменять, заменять, дописать (скопировать дочерние элементы копируемого элемента-каталога в элемент-каталог-дубликат)</param>
        /// <param name="BeforeCopyItemEvent">событие копирования элемента. Возникает Перед копированием</param>
        /// <param name="AfterCopyItemEvent">событие копирования элемента. Возникает После копирования</param>
        public List<int> CopyItems(List<int> moveIndList, DBItem destDir, GetActionWithDuplDelegate GetActionWithDupl, OnMoveDelegate BeforeCopyItemEvent, OnMoveDelegate AfterCopyItemEvent)
        {
            return CopyItems(moveIndList, destDir, GetActionWithDupl, BeforeCopyItemEvent, AfterCopyItemEvent, false);
        }

        public delegate void OnMoveDelegate(DBItem MoveItem, MoveAction Action);
        /// <summary>Дейсвие копирования/переноса: Просто добавлен, Заменен дубликат, дубликат-каталог дописан дочерними элементами</summary>
        public enum MoveAction {Add, Replace, AppendItems }
        public delegate ActionWithDupl GetActionWithDuplDelegate(DBItem oldItem, DBItem newItem);
        public enum ActionWithDupl { NotReplase, Replace, AppendItems }

        /// <summary>
        /// Копирование или перенос дочерних элементов из элемента srsDir в элемент destDir
        /// Возаращает индексы всех Полностью скопированных элементов (частично скопированные, то есть подкаталоги, в которых остались не скопированные элементы не входят)
        /// </summary>
        /// <param name="moveIndList">список индексов элементов, которые нужно скопировать</param>
        /// <param name="destDir">элемент, куда копируются дочерние элементы</param>
        /// <param name="GetActionWithDupl">функция, возвращающая, что делать с дубликатом: Не заменять, заменять, дописать (скопировать дочерние элементы копируемого элемента-каталога в элемент-каталог-дубликат)</param>
        /// <param name="AfterMoveItemEvent">событие копирования/переноса элемента. Возникает После переноса</param>
        /// <param name="bMove">Не копировать, а переносить. То есть, по завершению копирования, удалить полностью скопированные элементы из srsDir </param>
        /// <returns></returns>
        List<int> CopyItems(List<int> moveIndList, DBItem destDir, GetActionWithDuplDelegate GetActionWithDupl, OnMoveDelegate BeforeMoveItemEvent, OnMoveDelegate AfterMoveItemEvent, bool bMove)
        {
            DBItem srsDir = this;

            if (!srsDir.IsDirectory) throw new Exception("Элемент из которого переносятся дочерние элементы не является каталогом");
            if (!destDir.IsDirectory) throw new Exception("Элемент в который переносятся дочерние элементы не является каталогом");

            List<int> fullCopyIndL = new List<int>(); // индексы всех Полностью скопированных элементов
            List<DBItem> removeL = new List<DBItem>(); // элементы, которые необходимо удалить (если предполагается перемещение, а не копирование)
           
            for (int i = 0; i < moveIndList.Count; i++)
            {
                int ind = moveIndList[i];
                DBItem moveItem = srsDir.Items[ind];

                // проверка уникальности
                int duplInd = destDir.IndexOfRecord(moveItem.NameExt);
                bool bAppendItems = false; // элемент был дополнен
                bool bFullCopy = true; // элемент скопирован полностью

                #region *** дубликат *********************
                if (duplInd != -1)
                {
                    DBItem oldItem = destDir.Items[duplInd];
                    ActionWithDupl actn = GetActionWithDupl(oldItem, moveItem);
                    // если переносится каталог
                    if (actn == ActionWithDupl.AppendItems)
                    {
                        bFullCopy = false;

                        // в папку с тем же именем просто дописываются элементы
                        List<int> copyIndL = new List<int>();
                        for (int d = 0; d < moveItem.Items.Count; d++)
                            copyIndL.Add(d);
                        List<int> IndL = moveItem.CopyItems(copyIndL, oldItem, GetActionWithDupl, BeforeMoveItemEvent, AfterMoveItemEvent, bMove);
                        if (IndL.Count == copyIndL.Count) bFullCopy = true;
                        bAppendItems = true;
                    }
                    // если переносится не каталог
                    else
                    {
                        // если стоит не заменять дубликат
                        if (actn == ActionWithDupl.NotReplase)
                            continue;
                        else
                            destDir.Items.RemoveAt(duplInd);
                    }
                }
                #endregion *** дубликат *********************

                // если небыло просто дополнения дубликата, добавляем новый элемент
                if (!bAppendItems)
                {
                    if (BeforeMoveItemEvent != null) BeforeMoveItemEvent(moveItem, duplInd == -1 ? MoveAction.Add : MoveAction.Replace);
                    moveItem.Parent = destDir;
                    destDir.Items.Add(moveItem);
                }

                // если элемент был скопирован или дополнен полностью
                if (bFullCopy)
                {
                    fullCopyIndL.Add(ind);
                    removeL.Add(moveItem);
                }

                if (AfterMoveItemEvent != null)
                {
                    MoveAction act = MoveAction.Add;
                    if (duplInd != -1) act = bAppendItems? MoveAction.AppendItems: MoveAction.Replace;
                    AfterMoveItemEvent(moveItem, act);
                }

            }

            // если что-то скопировано
            if (removeL.Count > 0)
            {
                destDir.Items.Sort(); // сортировка в папке, куда перенесено

                // если сказано перемещать (а не копировать), удаление перенесенных
                if (bMove)
                    for (int i = 0; i < removeL.Count; i++) { srsDir.Items.Remove(removeL[i]); }

            }
            return fullCopyIndL;
        }

        public override string ToString()
        {
            return FullPath;
        }

    }

}
